/**
 * websocket逻辑
 */

import { OnDestory, OnInit } from '@core/types';
import { createLogger } from '@nodejs/logger';
import { del } from '@nodejs/redis';
import { IncomingMessage, Server } from 'http';
import type { Logger } from 'pino';
import { v4 } from 'uuid';
import { WebSocket, WebSocketServer } from 'ws';
import { RedisHashMap } from '../redis/index'
import { parseJSON } from '@tools/core';
import { requestToBuffer } from '../proto';
import { publishToTopic } from '@nodejs/mq';
import { useSecret } from '../utils';
import jwt from 'jsonwebtoken';

export class WebSocketServerPlugin implements OnInit, OnDestory {
    logger: Logger;
    pluginName: string = `WebSocketServerPlugin`
    webSocketServer: WebSocketServer;
    isDestoried: boolean = false;
    wsMap: RedisHashMap;
    rootProto: protobuf.Root;
    constructor(public server: Server, public appId: string) { }

    async onInit(): Promise<void> {
        this.logger = await createLogger(this.pluginName)
        this.webSocketServer = new WebSocketServer({ server: this.server })
        // 删除在线列表
        this.webSocketServer.on('close', async () => {
            if (this.isDestoried) return;
            await this.onInit()
        });
        this.webSocketServer.on('error', error => this.logger.error(error))
        this.webSocketServer.on('connection', (socket, request) => {
            this.onConnection(socket, request)
        })
        this.wsMap = await RedisHashMap.useRedisHashMap(`ws.online.${this.appId}`)
    }

    async onConnection(socket: WebSocket, request: IncomingMessage) {
        const url = request.url
        if (url) {
            const getParams = this.parseUrl(url)
            const uuid = getParams('uuid', v4())
            const name = getParams('name', 'main')
            const token = getParams('token', '')
            let user: any = null;
            try {
                const secret = useSecret()
                user = jwt.verify(token, secret)
            } catch (e) { }
            const ip = (request.headers['x-forwarded-for'] || request.socket.remoteAddress) as string;
            this.logger.info(`${name} ${ip} ${uuid} online`)
            // 记录uuid到redis
            await this.wsMap.set(`${name}.${uuid}`, { name, uuid, ip, uid: user?.uid || 0 })
            socket.on('ping', () => socket.pong())
            const pinginterval = 10 * 1000;
            const ponginterval = 10 * 1000;
            const pingTimeout = 10 * 1000;
            let pongTimeouter: any = null;
            const sendPing = () => {
                socket.ping(() => {
                    pongTimeouter = setTimeout(() => {
                        this.logger.info('Pong timeout error');
                        socket.terminate()
                    }, pingTimeout)
                });
            };
            const pingInterval: any = setInterval(sendPing, pinginterval + ponginterval);
            socket.on('pong', () => {
                if (pongTimeouter) clearTimeout(pongTimeouter)
            });
            socket.on('close', async (code, reason) => {
                await this.wsMap.delete(`${name}.${uuid}`)
                pingInterval && clearInterval(pingInterval)
                pongTimeouter && clearTimeout(pongTimeouter)
            });
            socket.on('error', e => {
                this.logger.error(e)
            });
            socket.send(JSON.stringify({ type: 'OPEN', payload: { uuid: uuid } }))
            // 处理消息
            socket.on('message', async (msg, isBinary) => {
                const str = msg.toString('utf8')
                const payload = parseJSON(str)
                const { type, payload: data } = payload;
                const msgId = v4()
                const version = payload.version || `v1`
                const buf = requestToBuffer(this.rootProto, {
                    appid: this.appId,
                    version: version,
                    name: payload.name || name,
                    reqid: uuid,
                    method: `ws`,
                    path: type,
                    ip: ip || '',
                    data: Buffer.from(JSON.stringify(data)),
                    headers: Buffer.from(JSON.stringify(request.headers)),
                });
                await publishToTopic(`request.topic`, `request.${payload.name || name}`, buf, {
                    appId: this.appId,
                    messageId: msgId,
                    timestamp: Date.now()
                });
            })
        }
    }

    private isEmpty(n: string | null): n is null {
        if (typeof n === 'string') {
            if (n === 'null') return true;
            if (n === 'undefined') return true;
            if (n.length === 0) return true;
            return false
        }
        return false;
    }

    private parseUrl(url: string) {
        const _ = new URL(this.toUrl(url))
        return (k: string, def: string): string => {
            const val = _.searchParams.get(k)
            if (this.isEmpty(val)) return def;
            return val;
        }
    }

    private toUrl(u: string) {
        if (u.startsWith('http')) {
            return u;
        }
        if (u.startsWith('//')) {
            return `http:${u}`
        }
        if (u.startsWith('/')) {
            return `http://localhost:3000${u}`
        }
        return u;
    }

    async onDestory(): Promise<void> {
        // 已被销毁
        this.isDestoried = true;
        this.webSocketServer.close((err) => this.logger.error(err))
        await del(`ws.online.${this.appId}`)
    }
}